Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

67장. 실전 4 — 서비스 하나를 다른 언어로 넘기기

앞의 세 실전은 하나의 Kotlin 모놀리스 이야기였다.

이번은 출발점이 다르다.

구 시스템   자체 프레임워크 + 자체 쿼리 빌더 (37장의 상황)
새 시스템   Kotlin + Spring Boot + JPA
대상        포인트 도메인
기간        10주

66장에서 12주에 걸쳐 포인트의 경계를 정리했다.
그 정리된 도메인을 이번에 넘긴다.

⚠️ 순서가 이렇다는 것이 중요하다.

경계 정리 12주 + 이관 10주.
이관만 따로 하려 했으면 22주가 아니라 더 걸렸을 것이다.


0주 — 이관 후보인지 확인

45장의 조건을 대조했다.

✅ 쓰기 주체 단독      point_histories 는 PointCommandService 만 (66장)
✅ 경계 넘는 조인 0     7곳 전부 제거 (66장)
✅ 나가는 의존 0        역참조 3곳 제거 (66장)
⚠️ 인증 방식           세션이 구 시스템 자체 직렬화 형식

마지막이 걸렸다.

사람이 결정한 것 ① — 이관 전에 세션을 JWT로 먼저 바꾼다.

⚠️ 이 결정이 2주를 추가했다.

46장에서 말한 그대로다.
인증 방식을 먼저 확인해야 하는 이유가 여기 있다.


1~2주 — 계약 테스트

45장의 첫 단계다.

포인트 도메인의 공개 API 를 전부 찾아줘.
각각의 요청·응답 형식을 실제 트래픽 로그에서 확인해줘.
문서가 아니라 실제 응답 기준으로.

⚠️ 마지막 문장이 필요했다.

내부 문서에 적힌 응답 형식과
실제 응답이 세 곳에서 달랐다.

# tests/contract/point.yaml — 34건
- name: 잔액 조회
- name: 잔액 조회 - 없는 사용자
- name: 사용 - 정상
- name: 사용 - 잔액 부족
- name: 사용 - 만료 예정분 우선 차감
...

구 시스템에 돌려 전부 통과시켰다.

🔥 이 파일이 이관 전체의 기준이 된다.

36장의 특성화 테스트가
언어를 건너뛸 수 있는 형태로 바뀐 것이다.


3주 — 명세 추출

45장의 두 단계 중 첫 번째다.

포인트 도메인 코드를 읽고 동작 명세를 추출해줘.
구현 방식은 적지 말고 무엇을 하는지만.

분기 조건은 경계값까지 전부 적어줘.

명세를 뽑는 과정에서 세 가지가 드러났다.

⚠️ 발견

1. 만료 처리 분기 4개 중 status=3 경로가
   어떤 조건에서 도달하는지 코드상 불명확
2. 금액 반올림이 두 곳에서 다름 (floor / round)
3. 잔액 0인 사용자 조회 시 404 와 200 이 경로에 따라 갈림

사람이 결정한 것 ② — 셋 다 그대로 이관한다.

45장의 원칙이다.
이관 중에는 버그도 이관한다.

3번은 실제 버그로 보였지만
클라이언트가 그 동작에 의존하고 있을 수 있었다.

이관 후 티켓으로 넘겼다.


4주 — 매핑표

37장에서 만든 쿼리 빌더 레퍼런스가
여기서 두 번째로 쓰였다.

## 쿼리 빌더 → JPA 매핑

| 구 쿼리 빌더 | JPA | 주의 |
|---|---|---|
| `->limit(10)` | `PageRequest.of(0, 10)` | 구 빌더는 페이지당 개수 |
| `->first()` | `findFirst()` | 없으면 null |
| `->forUpdate()` | `@Lock(PESSIMISTIC_WRITE)` | |
| `->join(...)` | ⚠️ 사용 금지 | 66장에서 전부 제거됨 |

⚠️ 첫 줄이 실제로 사고를 막았다.

37장에서 경고한 그 함정이다.
이름은 같은데 의미가 다른 것.


5~7주 — 구현

@docs/point-spec.md 의 명세대로 구현해줘.

- 구 코드는 보지 마. 명세만 보고 구현해
- @order/ 의 기준 구현 구조를 따라
- JPA 엔티티와 Repository 를 쓴다
- 쿼리는 @docs/query-mapping.md 를 참고

🔥 “구 코드는 보지 마” 가 결과를 갈랐다.

시험 삼아 구 코드를 보여주고 한 번 시켜봤는데,
HashMap<String, Any> 를 반환하는 코드가 나왔다.

명세만 주니 sealed 결과 타입을 쓴 코드가 나왔다.
44장에서 말한 그 형태다.

계약 테스트 34건을 새 시스템에 돌렸다.

1차   19 / 34 통과
2차   31 / 34
3차   34 / 34

실패한 것들은 대부분 응답 필드 순서와
숫자 직렬화 형식이었다.


8주 — 섀도 트래픽

45장의 검증 단계다.

운영 요청을 복제해 새 시스템에도 보낸다.
새 시스템 응답은 버리고 구 시스템 응답만 사용자에게.
두 응답을 비교해 차이를 로그로 남긴다.

3일간 돌린 결과다.

요청 82,000건 / 차이 1,284건

숫자 포맷      "5000" vs 5000       811건   (직렬화)
null 처리      필드 누락 vs null     402건   (직렬화)
정렬 순서      동률 시 순서 다름       58건   (쿼리)
⚠️ 금액        소수점 처리 차이        13건   (로직)

앞의 셋은 형식이라 맞췄다.

마지막 13건이 진짜였다.

차이 원인: 만료 예정 포인트 계산에서
구 시스템은 일 단위 절삭, 새 시스템은 시간 단위 계산

명세 추출 단계에서 놓친 것이다.

사람이 결정한 것 ③ — 구 시스템 동작에 맞춘다.

45장의 판단표 그대로다.
새 시스템이 더 정확해 보여도 일단 맞춘다.


9주 — 라우팅 전환

46장의 단계다.

1일차   읽기 API, 내부 직원만
3일차   읽기 API, userId 해시 기준 5%
5일차   읽기 API, 30%
7일차   읽기 API, 100%

⚠️ 3일차에 문제가 하나 났다.

같은 사용자가 두 시스템을 오갔다.
원인: 게이트웨이 규칙이 요청마다 랜덤이었음
증상: 잔액 캐시가 다르게 보임

46장에서 경고한 고정 배정 문제다.
그날 규칙을 해시 기준으로 바꿨다.

쓰기 전환은 다음 주로 미뤘다.

사람이 결정한 것 ④ — 읽기 전환을 1주 더 관찰한 뒤 쓰기로.


10주 — 쓰기 전환

진짜 관문이다.

## 전환 순서

1. point_histories 쓰기 주체를 새 시스템으로 (테이블 단위)
2. 구 시스템의 해당 쓰기 경로를 차단 (코드는 남김)
3. 24시간 관찰
4. point_balances 동일 절차

46장의 “한 테이블의 쓰기는 한 시스템만” 규칙이다.

전환 직후 30분간 사람이 지켜봤다.

확인 항목
- 적립·차감 건수가 평소와 같은가
- 잔액 합계가 어긋나지 않는가
- 구 시스템 로그에 쓰기 시도가 없는가

세 번째에서 하나 걸렸다.

⚠️ 배치가 아직 구 시스템 경로로 쓰고 있음
   PointExpireJob (66장에서 legacy 에 있다고 확인했던 그것)

66장 1주차 조사에서 발견해 기록해뒀던 항목이다.
이관 범위에서 제외했다가 여기서 걸렸다.

사람이 결정한 것 ⑤ — 배치를 즉시 중단하고
다음 주기 전까지 새 시스템 쪽에 구현.

🔥 34장에서 “진입점은 Controller만이 아니다” 라고 한 이유다.


그 뒤

11주~   공존 기간 (구 코드 유지, 트래픽 0)
13주    라우트 제거
15주    구 코드 제거

46장의 3단계 삭제 절차다.

라우트만 제거하고 2주 관찰하는 동안
호출한 곳은 없었다.


10주를 돌아보면

계약 테스트         2주   ← 가장 값진 투자
명세 추출           1주
매핑표              1주
구현                3주   ← Agent 비중 가장 높음
섀도 트래픽 검증     1주
라우팅 전환         2주

⚠️ 구현이 3주로 가장 짧지 않다는 점이 이 장의 요지다.

45장에서 말한 그대로다.

번역은 싸졌고
같음을 증명하는 일이 비싸졌다.

검증과 전환에 6주,
번역에 3주였다.

사람이 결정한 것은 다섯 곳이었고
전부 되돌리기 어렵거나 도메인 판단이었다.


이 경험에서 하네스에 반영한 것

62장의 개선 루프다.

## 이관 체크리스트 (다음 도메인용)

- [ ] 인증 방식이 양쪽에서 통하는가 ← 1차 때 2주 손해
- [ ] 배치·스케줄러가 이 도메인 테이블을 쓰는가 ← 10주차 사고
- [ ] 게이트웨이 배정이 고정인가 ← 9주차 사고
- [ ] 시간·날짜 계산 단위를 명세에 명시했는가 ← 8주차 13건

네 항목 모두 이번에 겪은 것이다.

다음 도메인은 이 목록을 가지고 시작한다.

🔥 이것이 이 책 전체의 방식이다.

사고가 날 때마다 하네스가 두꺼워지고,
다음 작업이 조금 더 안전해진다.


이 장의 핵심

  • 경계 정리 12주가 선행됐기에 이관이 10주로 끝났다
  • 인증 방식을 먼저 확인하지 않아 2주를 손해 봤다
  • 계약 테스트를 실제 응답 기준으로 만들어야 한다 — 문서와 달랐다
  • 명세 추출 단계에서 이관 전에 물어볼 것 세 가지가 드러났다
  • 발견한 버그를 그대로 이관했다 — 클라이언트가 의존하고 있을 수 있다
  • “구 코드는 보지 마” 가 결과물의 언어 품질을 갈랐다
  • 섀도 트래픽 차이 1,284건 중 진짜는 13건이었다
  • 그 13건은 명세 추출에서 놓친 시간 단위 문제였다
  • 게이트웨이 배정이 랜덤이면 같은 사용자가 두 시스템을 오간다
  • 쓰기 전환에서 배치 호출이 걸렸다 — 진입점은 HTTP만이 아니다
  • 번역 3주, 검증과 전환 6주였다
  • 겪은 사고 네 개가 다음 도메인의 체크리스트가 됐다